ট্রি ট্রাভার্সালের জন্য জেনেরিক ভিজিটর প্যাটার্নে দক্ষতা অর্জন করুন। আরও নমনীয় ও রক্ষণাবেক্ষণযোগ্য কোডের জন্য ট্রি স্ট্রাকচার থেকে অ্যালগরিদম আলাদা করার একটি সম্পূর্ণ গাইড।
নমনীয় ট্রি ট্রাভার্সাল আনলক করা: জেনেরিক ভিজিটর প্যাটার্নের গভীরে
সফটওয়্যার ইঞ্জিনিয়ারিংয়ের জগতে, আমরা প্রায়শই হায়ারার্কিকাল, ট্রি-এর মতো স্ট্রাকচারে সাজানো ডেটার সম্মুখীন হই। কম্পাইলাররা আমাদের কোড বোঝার জন্য যে অ্যাবস্ট্রাক্ট সিনট্যাক্স ট্রি (AST) ব্যবহার করে, ওয়েবকে চালিত করে যে ডকুমেন্ট অবজেক্ট মডেল (DOM), এমনকি সাধারণ ফাইল সিস্টেম পর্যন্ত, ট্রি সর্বত্র রয়েছে। এই স্ট্রাকচারগুলোর সাথে কাজ করার একটি মৌলিক কাজ হলো ট্রাভার্সাল: কোনো অপারেশন সম্পাদনের জন্য প্রতিটি নোড ভিজিট করা। তবে চ্যালেঞ্জটি হলো, এটি এমনভাবে করা যা পরিষ্কার, রক্ষণাবেক্ষণযোগ্য এবং প্রসারণযোগ্য।
প্রচলিত পদ্ধতিগুলো প্রায়শই নোড ক্লাসগুলোর মধ্যে সরাসরি অপারেশনাল লজিক যুক্ত করে দেয়। এটি মনোলিথিক, টাইটলি-কাপল্ড কোডের জন্ম দেয় যা সফটওয়্যার ডিজাইনের মূল নীতিগুলো লঙ্ঘন করে। একটি নতুন অপারেশন, যেমন একটি প্রিটি-প্রিন্টার বা একটি ভ্যালিডেটর যোগ করতে হলে, আপনাকে প্রতিটি নোড ক্লাস পরিবর্তন করতে হয়, যা সিস্টেমকে ভঙ্গুর এবং রক্ষণাবেক্ষণের জন্য কঠিন করে তোলে।
ক্লাসিক ভিজিটর ডিজাইন প্যাটার্নটি যে অবজেক্টগুলোর ওপর অ্যালগরিদমগুলো কাজ করে, তাদের থেকে আলাদা করে একটি শক্তিশালী সমাধান দেয়। কিন্তু ক্লাসিক প্যাটার্নেরও কিছু সীমাবদ্ধতা রয়েছে, বিশেষ করে যখন প্রসারণযোগ্যতার কথা আসে। এখানেই জেনেরিক ভিজিটর প্যাটার্ন, বিশেষত যখন ট্রি ট্রাভার্সালে প্রয়োগ করা হয়, তখন তার নিজস্বতা প্রমাণ করে। জেনেরিক, টেমপ্লেট এবং ভ্যারিয়েন্টের মতো আধুনিক প্রোগ্রামিং ভাষার বৈশিষ্ট্যগুলো ব্যবহার করে, আমরা যেকোনো ট্রি স্ট্রাকচার প্রসেস করার জন্য একটি অত্যন্ত নমনীয়, পুনঃব্যবহারযোগ্য এবং শক্তিশালী সিস্টেম তৈরি করতে পারি।
এই গভীর আলোচনাটি আপনাকে ক্লাসিক ভিজিটর প্যাটার্ন থেকে একটি অত্যাধুনিক, জেনেরিক ইমপ্লিমেন্টেশনের যাত্রাপথে পথ দেখাবে। আমরা অন্বেষণ করব:
- ক্লাসিক ভিজিটর প্যাটার্ন এবং এর অন্তর্নিহিত চ্যালেঞ্জগুলোর ওপর একটি পুনরালোচনা।
- একটি জেনেরিক পদ্ধতির বিবর্তন যা অপারেশনগুলোকে আরও বিচ্ছিন্ন করে।
- একটি জেনেরিক ট্রি ট্রাভার্সাল ভিজিটরের বিস্তারিত, ধাপে ধাপে বাস্তবায়ন।
- অপারেশনাল লজিক থেকে ট্রাভার্সাল লজিক আলাদা করার গভীর সুবিধা।
- বাস্তব-বিশ্বের অ্যাপ্লিকেশন যেখানে এই প্যাটার্নটি বিশাল মূল্য প্রদান করে।
আপনি একটি কম্পাইলার, একটি স্ট্যাটিক অ্যানালাইসিস টুল, একটি UI ফ্রেমওয়ার্ক, বা জটিল ডেটা স্ট্রাকচারের ওপর নির্ভরশীল যেকোনো সিস্টেম তৈরি করুন না কেন, এই প্যাটার্নে দক্ষতা অর্জন আপনার আর্কিটেকচারাল চিন্তাভাবনা এবং আপনার কোডের মানকে উন্নত করবে।
ক্লাসিক ভিজিটর প্যাটার্নের পুনরালোচনা
জেনেরিক বিবর্তনকে উপলব্ধি করার আগে, আমাদের এর ভিত্তি সম্পর্কে একটি শক্ত ধারণা থাকতে হবে। ভিজিটর প্যাটার্ন, যেমনটি 'গ্যাং অফ ফোর' তাদের বিখ্যাত বই ডিজাইন প্যাটার্নস: এলিমেন্টস অফ রিইউজেবল অবজেক্ট-ওরিয়েন্টেড সফটওয়্যার-এ বর্ণনা করেছেন, এটি একটি বিহেভিওরাল প্যাটার্ন যা আপনাকে বিদ্যমান অবজেক্ট স্ট্রাকচার পরিবর্তন না করে সেগুলিতে নতুন অপারেশন যোগ করার অনুমতি দেয়।
এটি যে সমস্যার সমাধান করে
কল্পনা করুন আপনার কাছে একটি সাধারণ গাণিতিক এক্সপ্রেশন ট্রি আছে যা বিভিন্ন ধরনের নোড নিয়ে গঠিত, যেমন NumberNode (একটি লিটারাল ভ্যালু) এবং AdditionNode (দুটি সাব-এক্সপ্রেশনের যোগফল উপস্থাপন করে)। আপনি এই ট্রিতে বিভিন্ন ধরনের স্বতন্ত্র অপারেশন সম্পাদন করতে চাইতে পারেন:
- ইভ্যালুয়েশন: এক্সপ্রেশনটির চূড়ান্ত সাংখ্যিক ফলাফল গণনা করা।
- প্রিটি প্রিন্টিং: একটি সহজে পাঠযোগ্য স্ট্রিং রিপ্রেজেন্টেশন তৈরি করা, যেমন "(5 + 3)"।
- টাইপ চেকিং: যাচাই করা যে সংশ্লিষ্ট টাইপগুলোর জন্য অপারেশনগুলো বৈধ।
সাধারণ পদ্ধতিটি হবে `evaluate()`, `print()` এবং `typeCheck()` এর মতো মেথডগুলো বেস `Node` ক্লাসে যোগ করা এবং প্রতিটি কঙ্ক্রিট নোড ক্লাসে সেগুলোকে ওভাররাইড করা। এটি নোড ক্লাসগুলোকে সম্পর্কহীন লজিক দিয়ে ভারাক্রান্ত করে তোলে। প্রতিবার যখন আপনি একটি নতুন অপারেশন তৈরি করবেন, আপনাকে হায়ারার্কির প্রতিটি নোড ক্লাস স্পর্শ করতে হবে। এটি ওপেন/ক্লোজড প্রিন্সিপল লঙ্ঘন করে, যা বলে যে সফটওয়্যার এন্টিটিগুলো এক্সটেনশনের জন্য খোলা থাকা উচিত কিন্তু পরিবর্তনের জন্য বন্ধ থাকা উচিত।
ক্লাসিক সমাধান: ডাবল ডিসপ্যাচ
ভিজিটর প্যাটার্ন এই সমস্যার সমাধান করে দুটি নতুন হায়ারার্কি প্রবর্তন করার মাধ্যমে: একটি ভিজিটর হায়ারার্কি এবং একটি এলিমেন্ট হায়ারার্কি (আমাদের নোডগুলো)। এর জাদুটি ডাবল ডিসপ্যাচ নামক একটি কৌশলের মধ্যে নিহিত।
এর মূল অংশগুলো হলো:
- এলিমেন্ট ইন্টারফেস (যেমন, `Node`): একটি `accept(Visitor v)` মেথড সংজ্ঞায়িত করে।
- কঙ্ক্রিট এলিমেন্টস (যেমন, `NumberNode`, `AdditionNode`): `accept` মেথডটি ইমপ্লিমেন্ট করে। ইমপ্লিমেন্টেশনটি সহজ: `visitor.visit(this);`।
- ভিজিটর ইন্টারফেস: প্রতিটি কঙ্ক্রিট এলিমেন্ট টাইপের জন্য একটি ওভারলোডেড `visit` মেথড ঘোষণা করে। উদাহরণস্বরূপ, `visit(NumberNode n)` এবং `visit(AdditionNode n)`।
- কঙ্ক্রিট ভিজিটর (যেমন, `EvaluationVisitor`, `PrintVisitor`): একটি নির্দিষ্ট অপারেশন সম্পাদনের জন্য `visit` মেথডগুলো ইমপ্লিমেন্ট করে।
এটি যেভাবে কাজ করে: আপনি `node.accept(myVisitor)` কল করেন। `accept` এর ভিতরে, নোডটি `myVisitor.visit(this)` কল করে। এই মুহুর্তে, কম্পাইলার `this` (যেমন, `AdditionNode`) এর কঙ্ক্রিট টাইপ এবং `myVisitor` (যেমন, `EvaluationVisitor`) এর কঙ্ক্রিট টাইপ জানে। তাই এটি সঠিক `visit` মেথডে ডিসপ্যাচ করতে পারে: `EvaluationVisitor::visit(AdditionNode*)`। এই দুই-ধাপের কলটি এমন কিছু অর্জন করে যা একটি একক ভার্চুয়াল ফাংশন কল করতে পারে না: দুটি ভিন্ন অবজেক্টের রানটাইম টাইপের উপর ভিত্তি করে সঠিক মেথড নির্ধারণ করা।
ক্লাসিক প্যাটার্নের সীমাবদ্ধতা
যদিও মার্জিত, ক্লাসিক ভিজিটর প্যাটার্নের একটি উল্লেখযোগ্য অসুবিধা রয়েছে যা ক্রমবিকাশমান সিস্টেমে এর ব্যবহারকে বাধাগ্রস্ত করে: এলিমেন্ট হায়ারার্কির অনমনীয়তা।
`Visitor` ইন্টারফেসে প্রতিটি `ConcreteElement` টাইপের জন্য একটি `visit` মেথড থাকে। আপনি যদি একটি নতুন নোড টাইপ যোগ করতে চান—ধরা যাক, একটি `MultiplicationNode`—আপনাকে বেস `Visitor` ইন্টারফেসে একটি নতুন `visit(MultiplicationNode n)` মেথড যোগ করতে হবে। এটি আপনাকে আপনার সিস্টেমে বিদ্যমান প্রতিটি কঙ্ক্রিট ভিজিটর ক্লাস আপডেট করতে বাধ্য করে এই নতুন মেথডটি ইমপ্লিমেন্ট করার জন্য। নতুন অপারেশন যোগ করার জন্য আমরা যে সমস্যার সমাধান করেছিলাম, তা এখন নতুন এলিমেন্ট টাইপ যোগ করার সময় আবার দেখা দেয়। সিস্টেমটি অপারেশনের দিকে পরিবর্তনের জন্য বন্ধ থাকলেও এলিমেন্টের দিকে পুরোপুরি খোলা।
এলিমেন্ট হায়ারার্কি এবং ভিজিটর হায়ারার্কির মধ্যে এই চক্রাকার নির্ভরতা একটি আরও নমনীয়, জেনেরিক সমাধান খোঁজার প্রধান অনুপ্রেরণা।
জেনেরিক বিবর্তন: একটি আরও নমনীয় পদ্ধতি
ক্লাসিক প্যাটার্নের মূল সীমাবদ্ধতা হলো ভিজিটর ইন্টারফেস এবং কঙ্ক্রিট এলিমেন্ট টাইপগুলোর মধ্যে স্ট্যাটিক, কম্পাইল-টাইম বন্ধন। জেনেরিক পদ্ধতি এই বন্ধন ভাঙতে চায়। মূল ধারণাটি হলো, সঠিক হ্যান্ডলিং লজিকে ডিসপ্যাচ করার দায়িত্বটি ওভারলোডেড মেথডগুলোর একটি অনমনীয় ইন্টারফেস থেকে সরিয়ে নেওয়া।
আধুনিক C++, এর শক্তিশালী টেমপ্লেট মেটাপ্রোগ্রামিং এবং `std::variant`-এর মতো স্ট্যান্ডার্ড লাইব্রেরি বৈশিষ্ট্যগুলোর সাথে, এটি বাস্তবায়নের জন্য একটি ব্যতিক্রমীভাবে পরিষ্কার এবং কার্যকর উপায় সরবরাহ করে। C# বা Java-এর মতো ভাষায়ও একই ধরনের পদ্ধতি অর্জন করা যেতে পারে রিফ্লেকশন বা জেনেরিক ইন্টারফেস ব্যবহার করে, যদিও এতে পারফরম্যান্সের ক্ষেত্রে কিছু আপোষ হতে পারে।
আমাদের লক্ষ্য এমন একটি সিস্টেম তৈরি করা যেখানে:
- নতুন নোড টাইপ যোগ করা স্থানীয়করণ করা হয় এবং এর জন্য সমস্ত বিদ্যমান ভিজিটর ইমপ্লিমেন্টেশনে ব্যাপক পরিবর্তনের প্রয়োজন হয় না।
- নতুন অপারেশন যোগ করা সহজ থাকে, যা ভিজিটর প্যাটার্নের মূল লক্ষ্যের সাথে সঙ্গতিপূর্ণ।
- ট্রাভার্সাল লজিক নিজেই (যেমন, প্রি-অর্ডার, পোস্ট-অর্ডার) জেনেরিকভাবে সংজ্ঞায়িত করা যায় এবং যেকোনো অপারেশনের জন্য পুনরায় ব্যবহার করা যায়।
এই তৃতীয় পয়েন্টটি আমাদের 'ট্রি ট্রাভার্সাল টাইপ ইমপ্লিমেন্টেশন'-এর চাবিকাঠি। আমরা শুধুমাত্র ডেটা স্ট্রাকচার থেকে অপারেশনকে আলাদা করব না, বরং আমরা ট্রাভার্স করার কাজটিকে অপারেট করার কাজ থেকে আলাদা করব।
C++ এ ট্রি ট্রাভার্সালের জন্য জেনেরিক ভিজিটর বাস্তবায়ন
আমরা আমাদের জেনেরিক ভিজিটর ফ্রেমওয়ার্ক তৈরি করতে আধুনিক C++ (C++17 বা তার পরবর্তী সংস্করণ) ব্যবহার করব। `std::variant`, `std::unique_ptr`, এবং টেমপ্লেটগুলোর সমন্বয় আমাদের একটি টাইপ-সেফ, কার্যকর এবং অত্যন্ত ভাবপ্রকাশক সমাধান দেয়।
ধাপ ১: ট্রি নোড স্ট্রাকচার সংজ্ঞায়িত করা
প্রথমে, আমাদের নোড টাইপগুলো সংজ্ঞায়িত করা যাক। একটি ভার্চুয়াল `accept` মেথড সহ একটি প্রচলিত ইনহেরিটেন্স হায়ারার্কির পরিবর্তে, আমরা আমাদের নোডগুলোকে সাধারণ স্ট্রাকট (struct) হিসাবে সংজ্ঞায়িত করব। তারপর আমরা আমাদের যেকোনো নোড টাইপ ধারণ করতে পারে এমন একটি সাম টাইপ (sum type) তৈরি করতে `std::variant` ব্যবহার করব।
একটি রিকার্সিভ স্ট্রাকচারের (একটি ট্রি যেখানে নোডগুলো অন্য নোড ধারণ করে) অনুমতি দেওয়ার জন্য, আমাদের একটি ইনডিরেকশন লেয়ার প্রয়োজন। একটি `Node` স্ট্রাকট ভ্যারিয়েন্টকে র্যাপ করবে এবং এর চাইল্ডদের জন্য `std::unique_ptr` ব্যবহার করবে।
ফাইল: `Nodes.h`
#include <memory> #include <variant> #include <vector> // Forward-declare the main Node wrapper struct Node; // Define the concrete node types as simple data aggregates struct NumberNode { double value; }; struct BinaryOpNode { enum class Operator { Add, Subtract, Multiply, Divide }; Operator op; std::unique_ptr<Node> left; std::unique_ptr<Node> right; }; struct UnaryOpNode { enum class Operator { Negate }; Operator op; std::unique_ptr<Node> operand; }; // Use std::variant to create a sum type of all possible node types using NodeVariant = std::variant<NumberNode, BinaryOpNode, UnaryOpNode>; // The main Node struct that wraps the variant struct Node { NodeVariant var; };
এই স্ট্রাকচারটি ইতোমধ্যেই একটি বিশাল উন্নতি। নোড টাইপগুলো হলো প্লেইন ওল্ড ডেটা স্ট্রাকট। তাদের ভিজিটর বা কোনো অপারেশন সম্পর্কে কোনো জ্ঞান নেই। একটি `FunctionCallNode` যোগ করতে, আপনি কেবল স্ট্রাকটটি সংজ্ঞায়িত করবেন এবং এটিকে `NodeVariant` অ্যালিয়াসে যুক্ত করবেন। এটি ডেটা স্ট্রাকচারের জন্য একটি একক পরিবর্তনের স্থান।
ধাপ ২: `std::visit` দিয়ে একটি জেনেরিক ভিজিটর তৈরি করা
`std::visit` ইউটিলিটি এই প্যাটার্নের ভিত্তিপ্রস্তর। এটি একটি কলযোগ্য অবজেক্ট (যেমন একটি ফাংশন, ল্যাম্বডা, বা `operator()` সহ একটি অবজেক্ট) এবং একটি `std::variant` নেয় এবং ভ্যারিয়েন্টে বর্তমানে সক্রিয় টাইপের উপর ভিত্তি করে কলযোগ্য অবজেক্টটির সঠিক ওভারলোডকে কল করে। এটি আমাদের টাইপ-সেফ, কম্পাইল-টাইম ডাবল ডিসপ্যাচ মেকানিজম।
একটি ভিজিটর এখন কেবল একটি স্ট্রাকট যা ভ্যারিয়েন্টের প্রতিটি টাইপের জন্য একটি ওভারলোডেড `operator()` ধারণ করে।
এটি বাস্তবে দেখার জন্য আসুন একটি সাধারণ প্রিটি-প্রিন্টার ভিজিটর তৈরি করি।
ফাইল: `PrettyPrinter.h`
#include "Nodes.h" #include <string> #include <iostream> struct PrettyPrinter { // Overload for NumberNode void operator()(const NumberNode& node) const { std::cout << node.value; } // Overload for UnaryOpNode void operator()(const UnaryOpNode& node) const { std::cout << "(- "; std::visit(*this, node.operand->var); // Recursive visit std::cout << ")"; } // Overload for BinaryOpNode void operator()(const BinaryOpNode& node) const { std::cout << "("; std::visit(*this, node.left->var); // Recursive visit switch (node.op) { case BinaryOpNode::Operator::Add: std::cout << " + "; break; case BinaryOpNode::Operator::Subtract: std::cout << " - "; break; case BinaryOpNode::Operator::Multiply: std::cout << " * "; break; case BinaryOpNode::Operator::Divide: std::cout << " / "; break; } std::visit(*this, node.right->var); // Recursive visit std::cout << ")"; } };
এখানে কী ঘটছে তা লক্ষ্য করুন। ট্রাভার্সাল লজিক (চাইল্ডদের ভিজিট করা) এবং অপারেশনাল লজিক (প্রথম বন্ধনী এবং অপারেটর প্রিন্ট করা) `PrettyPrinter`-এর ভিতরে একসাথে মিশ্রিত। এটি কার্যকরী, কিন্তু আমরা আরও ভালো করতে পারি। আমরা কী এবং কীভাবে এই দুটিকে আলাদা করতে পারি।
ধাপ ৩: অনুষ্ঠানের তারকা - জেনেরিক ট্রি ট্রাভার্সাল ভিজিটর
এখন, আমরা মূল ধারণাটি উপস্থাপন করছি: একটি পুনঃব্যবহারযোগ্য `TreeWalker` যা ট্রাভার্সাল স্ট্র্যাটেজিকে এনক্যাপসুলেট করে। এই `TreeWalker` নিজেই একটি ভিজিটর হবে, কিন্তু এর একমাত্র কাজ হলো ট্রি বরাবর হাঁটা। এটি অন্যান্য ফাংশন (ল্যাম্বডা বা ফাংশন অবজেক্ট) গ্রহণ করবে যা ট্রাভার্সালের নির্দিষ্ট সময়ে কার্যকর হবে।
আমরা বিভিন্ন স্ট্র্যাটেজি সমর্থন করতে পারি, তবে একটি সাধারণ এবং শক্তিশালী হলো 'প্রি-ভিজিট' (চাইল্ডদের ভিজিট করার আগে) এবং 'পোস্ট-ভিজিট' (চাইল্ডদের ভিজিট করার পরে) এর জন্য হুক সরবরাহ করা। এটি সরাসরি প্রি-অর্ডার এবং পোস্ট-অর্ডার ট্রাভার্সাল অ্যাকশনের সাথে মিলে যায়।
ফাইল: `TreeWalker.h`
#include "Nodes.h" #include <functional> template <typename PreVisitAction, typename PostVisitAction> struct TreeWalker { PreVisitAction pre_visit; PostVisitAction post_visit; // Base case for nodes with no children (terminals) void operator()(const NumberNode& node) { pre_visit(node); post_visit(node); } // Case for nodes with one child void operator()(const UnaryOpNode& node) { pre_visit(node); std::visit(*this, node.operand->var); // Recurse post_visit(node); } // Case for nodes with two children void operator()(const BinaryOpNode& node) { pre_visit(node); std::visit(*this, node.left->var); // Recurse left std::visit(*this, node.right->var); // Recurse right post_visit(node); } }; // Helper function to make creating the walker easier template <typename Pre, typename Post> auto make_tree_walker(Pre pre, Post post) { return TreeWalker<Pre, Post>{pre, post}; }
এই `TreeWalker` হলো পৃথকীকরণের একটি उत्कृष्ट कृति। এটি প্রিন্টিং, ইভ্যালুয়েশন, বা টাইপ-চেকিং সম্পর্কে কিছুই জানে না। এর একমাত্র উদ্দেশ্য হলো ট্রির একটি ডেপথ-ফার্স্ট ট্রাভার্সাল সম্পাদন করা এবং প্রদত্ত হুকগুলো কল করা। `pre_visit` অ্যাকশনটি প্রি-অর্ডারে এবং `post_visit` অ্যাকশনটি পোস্ট-অর্ডারে সম্পাদিত হয়। কোন ল্যাম্বডা ইমপ্লিমেন্ট করতে হবে তা বেছে নিয়ে ব্যবহারকারী যেকোনো ধরনের অপারেশন সম্পাদন করতে পারে।
ধাপ ৪: শক্তিশালী, ডিকাপল্ড অপারেশনের জন্য `TreeWalker` ব্যবহার করা
এখন, আসুন আমাদের `PrettyPrinter` রিফ্যাক্টর করি এবং আমাদের নতুন জেনেরিক `TreeWalker` ব্যবহার করে একটি `EvaluationVisitor` তৈরি করি। অপারেশনাল লজিক এখন সাধারণ ল্যাম্বডা হিসাবে প্রকাশ করা হবে।
ল্যাম্বডা কলগুলোর মধ্যে স্টেট পাস করার জন্য (যেমন ইভ্যালুয়েশন স্ট্যাক), আমরা ভেরিয়েবলগুলোকে রেফারেন্স দ্বারা ক্যাপচার করতে পারি।
ফাইল: `main.cpp`
#include "Nodes.h" #include "TreeWalker.h" #include <iostream> #include <string> #include <vector> // Helper for creating a generic lambda that can handle any node type template<class... Ts> struct Overloaded : Ts... { using Ts::operator()...; }; template<class... Ts> Overloaded(Ts...) -> Overloaded<Ts...>; int main() { // Let's build a tree for the expression: (5 + (10 * 2)) auto num5 = std::make_unique<Node>(Node{NumberNode{5.0}}); auto num10 = std::make_unique<Node>(Node{NumberNode{10.0}}); auto num2 = std::make_unique<Node>(Node{NumberNode{2.0}}); auto mult = std::make_unique<Node>(Node{BinaryOpNode{ BinaryOpNode::Operator::Multiply, std::move(num10), std::move(num2) }}); auto root = std::make_unique<Node>(Node{BinaryOpNode{ BinaryOpNode::Operator::Add, std::move(num5), std::move(mult) }}); std::cout << "--- Pretty Printing Operation ---\n"; auto printer_pre_visit = Overloaded { [](const NumberNode& node) { std::cout << node.value; }, [](const UnaryOpNode&) { std::cout << "(- "; }, [](const BinaryOpNode&) { std::cout << "("; } }; auto printer_post_visit = Overloaded { [](const NumberNode&) {}, // Do nothing [](const UnaryOpNode&) { std::cout << ")"; }, [](const BinaryOpNode& node) { switch (node.op) { case BinaryOpNode::Operator::Add: std::cout << " + "; break; case BinaryOpNode::Operator::Subtract: std::cout << " - "; break; case BinaryOpNode::Operator::Multiply: std::cout << " * "; break; case BinaryOpNode::Operator::Divide: std::cout << " / "; break; } } }; // এটি কাজ করবে না কারণ চাইল্ডদের প্রি এবং পোস্টের মাঝে ভিজিট করা হয়। // আসুন ইন-অর্ডার প্রিন্টের জন্য ওয়াকারটিকে আরও নমনীয় করি। // প্রিটি প্রিন্টিংয়ের জন্য একটি ভালো পদ্ধতি হলো একটি "ইন-ভিজিট" হুক থাকা। // সরলতার জন্য, আসুন প্রিন্টিং লজিকটি কিছুটা পুনর্গঠন করি। // অথবা আরও ভালো, আসুন একটি ডেডিকেটেড PrintWalker তৈরি করি। আপাতত প্রি/পোস্টে থাকি এবং ইভ্যালুয়েশন দেখাই যা আরও উপযুক্ত। std::cout << "\n--- Evaluation Operation ---\n"; std::vector<double> eval_stack; auto eval_pre_visit = [](const auto&){}; // Do nothing on pre-visit auto eval_post_visit = Overloaded { [&](const NumberNode& node) { eval_stack.push_back(node.value); }, [&](const UnaryOpNode& node) { double operand = eval_stack.back(); eval_stack.pop_back(); eval_stack.push_back(-operand); }, [&](const BinaryOpNode& node) { double right = eval_stack.back(); eval_stack.pop_back(); double left = eval_stack.back(); eval_stack.pop_back(); switch(node.op) { case BinaryOpNode::Operator::Add: eval_stack.push_back(left + right); break; case BinaryOpNode::Operator::Subtract: eval_stack.push_back(left - right); break; case BinaryOpNode::Operator::Multiply: eval_stack.push_back(left * right); break; case BinaryOpNode::Operator::Divide: eval_stack.push_back(left / right); break; } } }; auto evaluator = make_tree_walker(eval_pre_visit, eval_post_visit); std::visit(evaluator, root->var); std::cout << "Evaluation result: " << eval_stack.back() << std::endl; return 0; }
ইভ্যালুয়েশন লজিকটি দেখুন। এটি পোস্ট-অর্ডার ট্রাভার্সালের জন্য একটি নিখুঁত মিল। আমরা কেবল তখনই একটি অপারেশন সম্পাদন করি যখন এর চাইল্ডদের মান গণনা করে স্ট্যাকে পুশ করা হয়। `eval_post_visit` ল্যাম্বডা `eval_stack` ক্যাপচার করে এবং ইভ্যালুয়েশনের জন্য সমস্ত লজিক ধারণ করে। এই লজিকটি নোড সংজ্ঞা এবং `TreeWalker` থেকে সম্পূর্ণ আলাদা। আমরা উদ্বেগের একটি সুন্দর ত্রি-মুখী পৃথকীকরণ অর্জন করেছি: ডেটা স্ট্রাকচার (নোডস), ট্রাভার্সাল অ্যালগরিদম (`TreeWalker`), এবং অপারেশন লজিক (ল্যাম্বডাস)।
জেনেরিক ভিজিটর পদ্ধতির সুবিধা
এই বাস্তবায়ন কৌশলটি উল্লেখযোগ্য সুবিধা প্রদান করে, বিশেষত বড় আকারের, দীর্ঘস্থায়ী সফটওয়্যার প্রকল্পগুলিতে।
অপ্রতিদ্বন্দ্বী নমনীয়তা এবং প্রসারণযোগ্যতা
এটিই প্রাথমিক সুবিধা। একটি নতুন অপারেশন যোগ করা খুবই সহজ। আপনি কেবল নতুন এক সেট ল্যাম্বডা লিখবেন এবং সেগুলোকে `TreeWalker`-এ পাস করবেন। আপনি কোনো বিদ্যমান কোড স্পর্শ করবেন না। এটি ওপেন/ক্লোজড প্রিন্সিপলের সাথে পুরোপুরি সঙ্গতিপূর্ণ। একটি নতুন নোড টাইপ যোগ করার জন্য স্ট্রাকট যোগ করতে হবে এবং `std::variant` অ্যালিয়াস আপডেট করতে হবে—একটি একক, স্থানীয় পরিবর্তন—এবং তারপর যে ভিজিটরদের এটি হ্যান্ডেল করতে হবে তাদের আপডেট করতে হবে। কম্পাইলার আপনাকে সহায়কভাবে বলে দেবে ঠিক কোন ভিজিটরগুলোতে (ওভারলোডেড ল্যাম্বডা) এখন একটি ওভারলোড অনুপস্থিত।
উদ্বেগের উন্নত পৃথকীকরণ
আমরা তিনটি স্বতন্ত্র দায়িত্বকে বিচ্ছিন্ন করেছি:
- ডেটা রিপ্রেজেন্টেশন: `Node` স্ট্রাকটগুলো সরল, নিষ্ক্রিয় ডেটা কন্টেইনার।
- ট্রাভার্সাল মেকানিক্স: `TreeWalker` ক্লাস একচেটিয়াভাবে ট্রি স্ট্রাকচার নেভিগেট করার লজিকের মালিক। আপনি সহজেই সিস্টেমের অন্য কোনো অংশ পরিবর্তন না করে একটি `InOrderTreeWalker` বা `BreadthFirstTreeWalker` তৈরি করতে পারেন।
- অপারেশনাল লজিক: ওয়াকারে পাস করা ল্যাম্বডাগুলো একটি নির্দিষ্ট কাজের (ইভ্যালুয়েটিং, প্রিন্টিং, টাইপ চেকিং, ইত্যাদি) জন্য নির্দিষ্ট ব্যবসায়িক লজিক ধারণ করে।
এই পৃথকীকরণ কোডকে বোঝা, পরীক্ষা করা এবং রক্ষণাবেক্ষণ করা সহজ করে তোলে। প্রতিটি উপাদানের একটি একক, সুনির্দিষ্ট দায়িত্ব থাকে।
উন্নত পুনঃব্যবহারযোগ্যতা
`TreeWalker` অসীমভাবে পুনঃব্যবহারযোগ্য। ট্রাভার্সাল লজিক একবার লেখা হয় এবং এটি অসীম সংখ্যক অপারেশনে প্রয়োগ করা যেতে পারে। এটি কোড ডুপ্লিকেশন এবং নতুন প্রতিটি ভিজিটরে ট্রাভার্সাল লজিক পুনরায় বাস্তবায়নের ফলে উদ্ভূত বাগের সম্ভাবনা হ্রাস করে।
সংক্ষিপ্ত এবং ভাবপ্রকাশক কোড
আধুনিক C++ বৈশিষ্ট্যগুলোর সাথে, ফলস্বরূপ কোড প্রায়শই ক্লাসিক ভিজিটর ইমপ্লিমেন্টেশনের চেয়ে বেশি সংক্ষিপ্ত হয়। ল্যাম্বডাগুলো অপারেশনাল লজিককে যেখানে ব্যবহার করা হয় সেখানেই সংজ্ঞায়িত করার অনুমতি দেয়, যা সরল, স্থানীয় অপারেশনের জন্য পঠনযোগ্যতা উন্নত করতে পারে। এক সেট ল্যাম্বডা থেকে ভিজিটর তৈরির জন্য `Overloaded` হেল্পার স্ট্রাকট একটি সাধারণ এবং শক্তিশালী ইডিয়ম যা ভিজিটর সংজ্ঞাগুলোকে পরিষ্কার রাখে।
সম্ভাব্য সীমাবদ্ধতা এবং বিবেচ্য বিষয়
কোনো প্যাটার্নই সর্বরোগহর ঔষধ নয়। এর সাথে জড়িত সীমাবদ্ধতাগুলো বোঝা গুরুত্বপূর্ণ।
প্রাথমিক সেটআপের জটিলতা
`std::variant` এবং জেনেরিক `TreeWalker`-এর সাথে `Node` স্ট্রাকচারের প্রাথমিক সেটআপ একটি সাধারণ রিকার্সিভ ফাংশন কলের চেয়ে বেশি জটিল মনে হতে পারে। এই প্যাটার্নটি সেইসব সিস্টেমে সবচেয়ে বেশি সুবিধা দেয় যেখানে ট্রি স্ট্রাকচার স্থিতিশীল, কিন্তু সময়ের সাথে সাথে অপারেশনের সংখ্যা বাড়ার সম্ভাবনা থাকে। খুব সহজ, একবার ব্যবহারযোগ্য ট্রি প্রসেসিং কাজের জন্য এটি অতিরিক্ত হতে পারে।
পারফরম্যান্স
`std::visit` ব্যবহার করে C++ এ এই প্যাটার্নের পারফরম্যান্স চমৎকার। `std::visit` সাধারণত কম্পাইলার দ্বারা একটি অত্যন্ত অপ্টিমাইজড জাম্প টেবিল ব্যবহার করে বাস্তবায়িত হয়, যা ডিসপ্যাচকে অত্যন্ত দ্রুত করে তোলে—প্রায়শই ভার্চুয়াল ফাংশন কলের চেয়েও দ্রুত। অন্যান্য ভাষায় যা একই ধরনের জেনেরিক আচরণ অর্জনের জন্য রিফ্লেকশন বা ডিকশনারি-ভিত্তিক টাইপ লুকআপের উপর নির্ভর করতে পারে, সেখানে একটি ক্লাসিক, স্ট্যাটিক্যালি-ডিসপ্যাচড ভিজিটরের তুলনায় লক্ষণীয় পারফরম্যান্স ওভারহেড থাকতে পারে।
ভাষার ওপর নির্ভরশীলতা
এই নির্দিষ্ট বাস্তবায়নের সৌন্দর্য এবং কার্যকারিতা C++17 বৈশিষ্ট্যগুলোর ওপর ব্যাপকভাবে নির্ভরশীল। যদিও মূলনীতিগুলো স্থানান্তরযোগ্য, অন্যান্য ভাষায় বাস্তবায়নের বিবরণ ভিন্ন হবে। উদাহরণস্বরূপ, Java-তে, আধুনিক সংস্করণগুলোতে একটি সিলড ইন্টারফেস এবং প্যাটার্ন ম্যাচিং ব্যবহার করা যেতে পারে, অথবা পুরোনো সংস্করণগুলোতে একটি আরও ভার্বোস ম্যাপ-ভিত্তিক ডিসপ্যাচার ব্যবহার করা যেতে পারে।
বাস্তব-বিশ্বের অ্যাপ্লিকেশন এবং ব্যবহারের ক্ষেত্র
ট্রি ট্রাভার্সালের জন্য জেনেরিক ভিজিটর প্যাটার্ন শুধু একটি অ্যাকাডেমিক অনুশীলন নয়; এটি অনেক জটিল সফটওয়্যার সিস্টেমের মেরুদণ্ড।
- কম্পাইলার এবং ইন্টারপ্রেটার: এটি একটি আদর্শ ব্যবহারের ক্ষেত্র। একটি অ্যাবস্ট্রাক্ট সিনট্যাক্স ট্রি (AST) বিভিন্ন 'ভিজিটর' বা 'পাস' দ্বারা একাধিকবার ট্রাভার্স করা হয়। একটি সেমান্টিক অ্যানালাইসিস পাস টাইপ ত্রুটি পরীক্ষা করে, একটি অপটিমাইজেশন পাস ট্রিটিকে আরও কার্যকর করার জন্য পুনর্লিখন করে, এবং একটি কোড জেনারেশন পাস চূড়ান্ত ট্রি ট্রাভার্স করে মেশিন কোড বা বাইটকোড নির্গত করার জন্য। প্রতিটি পাস একই ডেটা স্ট্রাকচারের উপর একটি স্বতন্ত্র অপারেশন।
- স্ট্যাটিক অ্যানালাইসিস টুল: লিন্টার, কোড ফরমেটার এবং নিরাপত্তা স্ক্যানারের মতো টুলগুলো কোডকে একটি AST-তে পার্স করে এবং তারপর প্যাটার্ন খুঁজে বের করতে, স্টাইল নিয়ম প্রয়োগ করতে বা সম্ভাব্য দুর্বলতা সনাক্ত করতে এর উপর বিভিন্ন ভিজিটর চালায়।
- ডকুমেন্ট প্রসেসিং (DOM): যখন আপনি একটি XML বা HTML ডকুমেন্ট ম্যানিপুলেট করেন, তখন আপনি একটি ট্রি নিয়ে কাজ করছেন। একটি জেনেরিক ভিজিটর সমস্ত লিঙ্ক এক্সট্র্যাক্ট করতে, সমস্ত ছবি রূপান্তর করতে, বা ডকুমেন্টটিকে অন্য ফর্ম্যাটে সিরিয়ালাইজ করতে ব্যবহার করা যেতে পারে।
- UI ফ্রেমওয়ার্ক: আধুনিক UI ফ্রেমওয়ার্কগুলো ইউজার ইন্টারফেসকে একটি কম্পোনেন্ট ট্রি হিসাবে উপস্থাপন করে। রেন্ডারিং, স্টেট আপডেট প্রচার (যেমন React-এর রিকনসিলিয়েশন অ্যালগরিদমে), বা ইভেন্ট ডিসপ্যাচ করার জন্য এই ট্রি ট্রাভার্স করা প্রয়োজন।
- 3D গ্রাফিক্সের সিন গ্রাফ: একটি 3D সিন প্রায়শই অবজেক্টের একটি হায়ারার্কি হিসাবে উপস্থাপিত হয়। ট্রান্সফরমেশন প্রয়োগ, ফিজিক্স সিমুলেশন সম্পাদন এবং রেন্ডারিং পাইপলাইনে অবজেক্ট জমা দেওয়ার জন্য একটি ট্রাভার্সাল প্রয়োজন। একটি জেনেরিক ওয়াকার একটি রেন্ডারিং অপারেশন প্রয়োগ করতে পারে, তারপর একটি ফিজিক্স আপডেট অপারেশন প্রয়োগ করার জন্য পুনরায় ব্যবহার করা যেতে পারে।
উপসংহার: অ্যাবস্ট্রাকশনের একটি নতুন স্তর
জেনেরিক ভিজিটর প্যাটার্ন, বিশেষত যখন একটি ডেডিকেটেড `TreeWalker`-এর সাথে বাস্তবায়িত হয়, তখন এটি সফটওয়্যার ডিজাইনে একটি শক্তিশালী বিবর্তনকে প্রতিনিধিত্ব করে। এটি ভিজিটর প্যাটার্নের মূল প্রতিশ্রুতি—ডেটা এবং অপারেশনের পৃথকীকরণ—গ্রহণ করে এবং ট্রাভার্সালের জটিল লজিককেও আলাদা করে এটিকে উন্নত করে।
সমস্যাটিকে তিনটি স্বতন্ত্র, অর্থোগোনাল উপাদানে—ডেটা, ট্রাভার্সাল এবং অপারেশন—ভেঙে ফেলার মাধ্যমে, আমরা এমন সিস্টেম তৈরি করি যা আরও মডুলার, রক্ষণাবেক্ষণযোগ্য এবং শক্তিশালী হয়। মূল ডেটা স্ট্রাকচার বা ট্রাভার্সাল কোড পরিবর্তন না করে নতুন অপারেশন যোগ করার ক্ষমতা সফটওয়্যার আর্কিটেকচারের জন্য একটি বিশাল জয়। `TreeWalker` একটি পুনঃব্যবহারযোগ্য সম্পদে পরিণত হয় যা কয়েক ডজন বৈশিষ্ট্যকে শক্তি জোগাতে পারে, নিশ্চিত করে যে ট্রাভার্সাল লজিক যেখানেই ব্যবহৃত হোক না কেন তা সামঞ্জস্যপূর্ণ এবং সঠিক।
যদিও এটি বোঝা এবং সেটআপের জন্য একটি প্রাথমিক বিনিয়োগের প্রয়োজন, জেনেরিক ট্রি ট্রাভার্সাল ভিজিটর প্যাটার্ন একটি প্রকল্পের জীবনকাল জুড়ে তার সুফল প্রদান করে। যেকোনো ডেভেলপারের জন্য যারা জটিল হায়ারার্কিকাল ডেটা নিয়ে কাজ করেন, তাদের জন্য এটি পরিষ্কার, নমনীয় এবং দীর্ঘস্থায়ী কোড লেখার একটি অপরিহার্য টুল।